﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Content;
using ModelViewerAux;

namespace ModelViewer
{
    /// <summary>
    /// An encloser for an XNA model that we will use that includes support for
    /// bones, animation, and some manipulations.
    /// </summary>
    public class ViewerModel : IPlaceable
    {
        #region Fields

        /// <summary>
        /// The actual underlying XNA model
        /// </summary>
        private Model model = null;

        /// <summary>
        /// Extra data associated with the XNA model
        /// </summary>
        private ModelExtra modelExtra = null;

        /// <summary>
        /// The computed model center after loading
        /// </summary>
        Vector3 modelCenter = new Vector3(0, 0, 0);

        /// <summary>
        /// The computed model radius
        /// </summary>
        float modelRadius = 100;

        /// <summary>
        /// The model bones
        /// </summary>
        private List<Bone> bones = new List<Bone>();

        /// <summary>
        /// The model meshes
        /// </summary>
        private List<Mesh> meshes = new List<Mesh>();

        /// <summary>
        /// The model scaling (x, y, z)
        /// </summary>
        private Vector3 scale = new Vector3(1, 1, 1);

        /// <summary>
        /// The model orientation. XYZ Euler Angles
        /// </summary>
        private Vector3 rotation = new Vector3(0, 0, 0);

        /// <summary>
        /// The model position
        /// </summary>
        private Vector3 translation = new Vector3(0, 0, 0);
        
        /// <summary>
        /// Model enabled state
        /// </summary>
        private bool enabled = true;

        /// <summary>
        /// The model name
        /// </summary>
        private string name = "";

        /// <summary>
        /// We create one player per clip and assign the clip to it
        /// </summary>
        private List<ClipPlayer> clipPlayers = new List<ClipPlayer>();

        /// <summary>
        /// Do we draw the bones?
        /// </summary>
        private bool drawBones = false;



        #endregion

        #region Properties

        /// <summary>
        /// The actual underlying XNA model
        /// </summary>
        public Model Model
        {
            get { return model; }
        }

        /// <summary>
        /// Extra information associated with the model
        /// Mostly for skinning support
        /// </summary>
        public ModelExtra ModelExtra { get { return modelExtra; } }

        /// <summary>
        /// The center of the model as measured after loading
        /// </summary>
        public Vector3 ModelCenter { get { return modelCenter; } }

        /// <summary>
        /// The model radius as measured after loading
        /// </summary>
        public float ModelRadius { get { return modelRadius; } }

        /// <summary>
        /// The underlying bones for the model
        /// </summary>
        public List<Bone> Bones { get { return bones; } }

        /// <summary>
        /// The model meshes
        /// </summary>
        public List<Mesh> Meshes { get { return meshes; } }

        /// <summary>
        /// The model animation clips
        /// </summary>
        public List<AnimationClip> Clips { get { return modelExtra.Clips; } }

        /// <summary>
        /// We create one player per clip and assign the clip to it
        /// </summary>
        public List<ClipPlayer> ClipPlayers
        {
            get { return clipPlayers; }
        }

        /// <summary>
        /// The model scale - x,y,z
        /// </summary>
        public Vector3 Scale
        {
            get { return scale; }
            set { scale = value; }
        }

        /// <summary>
        /// Current model placement orientation
        /// Euler angles XYZ
        /// </summary>
        public Vector3 Rotation
        {
            get { return rotation; }
            set { rotation = value; }
        }

        /// <summary>
        /// Current model position
        /// </summary>
        public Vector3 Translation
        {
            get { return translation; }
            set { translation = value; }
        }

        /// <summary>
        /// Model name
        /// </summary>
        public string Name
        {
            get { return name; }
            set { name = value; }
        }

        /// <summary>
        /// Model is enabled for drawing
        /// </summary>
        public bool Enabled
        {
            get { return enabled; }
            set { enabled = value; }
        }

        /// <summary>
        /// Do we draw all of the bones?
        /// </summary>
        public bool DrawBones
        {
            get { return drawBones; }
            set { drawBones = value; }
        }

        #endregion

        #region Initialization and Loading

        /// <summary>
        /// Constructor. Creates the model from an XNA model
        /// </summary>
        /// <param name="model"></param>
        public ViewerModel(string name, Model model)
        {
            this.name = name;
            this.model = model;
            modelExtra = model.Tag as ModelExtra;
            ObtainBones();
            ObtainMeshes();
            ObtainClips();
            SetEffectParameters();
            MeasureModel();
            ComputeStatistics();
        }

        /// <summary>
        /// Set any parameters we want to set for the
        /// effects that draw this model.
        /// </summary>
        private void SetEffectParameters()
        {
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (Effect effect in mesh.Effects)
                {
                    if (effect is BasicEffect)
                    {
                        BasicEffect beffect = effect as BasicEffect;
                        beffect.EnableDefaultLighting();
                        beffect.PreferPerPixelLighting = true;
                    }

                    if (effect is SkinnedEffect)
                    {
                        SkinnedEffect seffect = effect as SkinnedEffect;
                        seffect.EnableDefaultLighting();
                        seffect.PreferPerPixelLighting = true;
                    }
                }

            }
        }

        #endregion

        #region Effects

        /// <summary>
        /// Get the name for an effect
        /// </summary>
        /// <param name="effect">The effect we want the name for</param>
        /// <returns></returns>
        public string GetEffectName(Effect effect)
        {
            return modelExtra.EffectNames[effect];
        }

        #endregion

        #region Bones Management

        private void ObtainBones()
        {
            bones.Clear();
            foreach (ModelBone bone in model.Bones)
            {
                Bone newBone = new Bone(bone.Name, bone.Transform);
                if (bone.Parent != null)
                {
                    newBone.Parent = bones[bone.Parent.Index];
                }

                // Set the skinning bind transform
                newBone.ComputeAbsoluteTransform();
                newBone.SkinTransform = Matrix.Invert(newBone.AbsoluteTransform);
                bones.Add(newBone);
            }
        }

        /// <summary>
        /// Find a bone in this model by name
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public Bone FindBone(string name)
        {
            foreach(Bone bone in Bones)
            {
                if (bone.Name == name)
                    return bone;
            }

            return null;
        }

        #endregion

        #region Meshes Management

        private void ObtainMeshes()
        {
            foreach (ModelMesh mesh in model.Meshes)
            {
                Mesh newMesh = new Mesh();
                newMesh.Name = mesh.Name;
                newMesh.ModelMesh = mesh;
                meshes.Add(newMesh);
            }
        }

        #endregion

        #region Animation Management

        /// <summary>
        /// Create a player for all clips and assign the clip to it.
        /// </summary>
        private void ObtainClips()
        {
            foreach (AnimationClip clip in Clips)
            {
                ClipPlayer player = new ClipPlayer(clip);
                clipPlayers.Add(player);

                // Automatically assign to this model
                player.Model = this;
            }
        }

        #endregion

        #region Reset

        public void ResetAll()
        {
            Translation = Vector3.Zero;
            Rotation = Vector3.Zero;
            Scale = Vector3.One;
            Enabled = true;

            foreach (Bone bone in bones)
            {
                bone.ResetAll();
            }
        }

        public void SetEnabled(bool enable)
        {
            Enabled = enable;

            foreach (Bone bone in bones)
            {
                bone.SetEnabled(enable);
            }
        }

        #endregion

        #region Drawing

        public void Draw(GraphicsDevice graphics, Camera camera, LineDraw lineDraw, Color ambientColor)
        {
            if (!Enabled)
                return;

            if (model != null)
            {
                Matrix world = Matrix.CreateScale(Scale) *
                    Matrix.CreateRotationX(Rotation.X) *
                    Matrix.CreateRotationY(Rotation.Y) *
                    Matrix.CreateRotationZ(Rotation.Z) *
                    Matrix.CreateTranslation(Translation);

                Matrix[] boneTransforms = new Matrix[bones.Count];

                for (int i = 0; i < bones.Count; i++)
                {
                    Bone bone = bones[i];
                    bone.ComputeAbsoluteTransform();

                    boneTransforms[i] = bone.AbsoluteTransform;
                }

                //
                // Determine the skin transforms from the skeleton
                //

                Matrix[] skeleton = new Matrix[modelExtra.Skeleton.Count];
                for (int s = 0; s < modelExtra.Skeleton.Count; s++)
                {
                    Bone bone = bones[modelExtra.Skeleton[s]];
                    skeleton[s] = bone.SkinTransform * bone.AbsoluteTransform;
                }

                // Draw the model.
                foreach (Mesh mesh in meshes)
                {
                    if (!mesh.Enabled)
                        continue;

                    ModelMesh modelMesh = mesh.ModelMesh;

                    if (!bones[modelMesh.ParentBone.Index].Enabled)
                        continue;

                    foreach (Effect effect in modelMesh.Effects)
                    {
                        if (effect is BasicEffect)
                        {
                            BasicEffect beffect = effect as BasicEffect;
                            beffect.World = boneTransforms[modelMesh.ParentBone.Index] * world;
                            beffect.View = camera.View;
                            beffect.Projection = camera.Projection;
                            beffect.EnableDefaultLighting();
                            beffect.PreferPerPixelLighting = true;
                            beffect.AmbientLightColor = ambientColor.ToVector3();
                        }

                        if (effect is SkinnedEffect)
                        {
                            SkinnedEffect seffect = effect as SkinnedEffect;
                            seffect.World = boneTransforms[modelMesh.ParentBone.Index] * world;
                            seffect.View = camera.View;
                            seffect.Projection = camera.Projection;
                            seffect.EnableDefaultLighting();
                            seffect.PreferPerPixelLighting = true;
                            seffect.SetBoneTransforms(skeleton);
                            seffect.AmbientLightColor = ambientColor.ToVector3();
                        }
                    }

                    modelMesh.Draw();
                }

                if (drawBones)
                {
                    // Draw the skeleton
                    foreach (Bone bone in bones)
                    {
                        lineDraw.DefaultColor = Color.Green;
                        if (bone.Parent != null)
                        {
                            lineDraw.Begin();
                            lineDraw.Vertex(Vector3.Transform(bone.Parent.AbsoluteTransform.Translation, world));
                            lineDraw.Vertex(Vector3.Transform(bone.AbsoluteTransform.Translation, world));

                            lineDraw.End();
                        }
                    }
                }
            }
        }

        #endregion

        #region Measurement and Accessories

        /// <summary>
        /// Whenever a new model is selected, we examine it to see how big
        /// it is and where it is centered. This lets us automatically zoom
        /// the display, so we can correctly handle models of any scale.
        /// </summary>
        void MeasureModel()
        {
            if (model == null)
            {
                // Handle case of no model
                modelCenter = new Vector3(0, 0, 0);
                modelRadius = 100;

                return;
            }

            Matrix[] boneTransforms = new Matrix[model.Bones.Count];
            model.CopyAbsoluteBoneTransformsTo(boneTransforms);

            // Compute an (approximate) model center position by
            // averaging the center of each mesh bounding sphere.
            modelCenter = Vector3.Zero;

            foreach (ModelMesh mesh in model.Meshes)
            {
                BoundingSphere meshBounds = mesh.BoundingSphere;
                Matrix transform = boneTransforms[mesh.ParentBone.Index];
                Vector3 meshCenter = Vector3.Transform(meshBounds.Center, transform);

                modelCenter += meshCenter;
            }

            modelCenter /= model.Meshes.Count;

            // Now we know the center point, we can compute the model radius
            // by examining the radius of each mesh bounding sphere.
            modelRadius = 0;

            foreach (ModelMesh mesh in model.Meshes)
            {
                BoundingSphere meshBounds = mesh.BoundingSphere;
                Matrix transform = boneTransforms[mesh.ParentBone.Index];
                Vector3 meshCenter = Vector3.Transform(meshBounds.Center, transform);

                float transformScale = transform.Forward.Length();
                
                float meshRadius = (meshCenter - modelCenter).Length() +
                                   (meshBounds.Radius * transformScale);

                modelRadius = Math.Max(modelRadius,  meshRadius);
            }
        }

        /// <summary>
        /// Set a reasonable initial camera configuration for this model.
        /// </summary>
        public void SetInitialCamera(Camera camera)
        {
            if (camera == null)
                return;

            camera.Center = modelCenter;
            camera.Eye = new Vector3(1, 1, 1) * modelRadius * 5;
            camera.ZNear = camera.CameraDistance - modelRadius * 2;
            camera.ZFar = camera.CameraDistance + modelRadius * 10;
            camera.Fov = MathHelper.ToRadians(20);
        }

        private int numVertices = 0;
        private int numTriangles = 0;
        private int numMeshes = 0;
        private int numMeshParts = 0;
        private int numEffects = 0;

        public int NumMeshes { get { return numMeshes; } }
        public int NumMeshParts { get { return numMeshParts; } }
        public int NumVertices { get { return numVertices; } }
        public int NumTriangles { get { return numTriangles; } }
        public int NumEffects { get { return numEffects; } }

        private void ComputeStatistics()
        {
            numMeshes = 0;
            numMeshParts = 0;
            numVertices = 0;
            numTriangles = 0;
            numEffects = 0;

            // Vertex buffers are shared, so this allows us to collect them all up in one place
            Dictionary<VertexBuffer, bool> vertexBuffers = new Dictionary<VertexBuffer, bool>();

            // Same issue applies for effects
            Dictionary<Effect, bool> effects = new Dictionary<Effect, bool>();

            foreach (ModelMesh mesh in model.Meshes)
            {
                numMeshes++;
                foreach (ModelMeshPart part in mesh.MeshParts)
                {
                    numMeshParts++;
                    effects[part.Effect] = true;
                    vertexBuffers[part.VertexBuffer] = true;

                    numTriangles += part.PrimitiveCount;
                }
            }

            foreach (VertexBuffer vb in vertexBuffers.Keys)
            {
                numVertices += vb.VertexCount;
            }

            numEffects = effects.Count;
        }

        #endregion


    }
}
